using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Oops.Xna.Framework.Graphics
{
	/// <summary>
	/// Defines a contract for <see cref="AnimationEvent"/> activation callbacks.
	/// </summary>
	public interface IAnimationPlayerCallback
	{
		/// <summary>
		/// Occurs when an <see cref="AnimationEvent"/> has been activated
		/// during playback of an <see cref="AnimationClip"/>.
		/// </summary>
		/// <param name="animationEvent">The activated <see cref="AnimationEvent"/>.</param>
		void OnEvent(GameTime gameTime, AnimationEvent animationEvent);
	}

	/// <summary>
	/// Maintains the current animation state of a <see cref="SkinningData"/> instance.
	/// </summary>
	public class AnimationPlayer
	{
		private SkinningData skinningData;

		private bool loop;
		private AnimationClip currentClip;
		private IAnimationPlayerCallback callback;

		private TimeSpan previousTime;
		private TimeSpan currentTime;
				
		/// <summary>
		/// Initializes a new instances of the <see cref="AnimationPlayer"/> class.
		/// </summary>
		/// <param name="skinningData">The associated <see cref="SkinningData"/>.</param>
		public AnimationPlayer(SkinningData skinningData)
		{
			// The skinning data must be specified.
			if (skinningData == null)
			{
				throw new ArgumentNullException("skinningData");
			}

			// Hold onto parameters.
			this.skinningData = skinningData;

			// Start looping on the first animation clip.
			this.currentClip = this.skinningData.AnimationClips[0];
			this.loop = true;
		}
		
		/// <summary>
		/// Gets the current <see cref="AnimationClip"/> of the <see cref="AnimationPlayer"/>.
		/// </summary>
		public AnimationClip CurrentClip
		{
			get { return this.currentClip; }
		}

		/// <summary>
		/// Gets or sets the current time of the <see cref="AnimationPlayer"/>.
		/// </summary>
		public TimeSpan CurrentTime
		{
			get { return this.currentTime; }
			set
			{
				if (this.currentTime != value)
				{
					if (value < TimeSpan.Zero || value >= this.CurrentClip.Duration)
					{
						throw new ArgumentOutOfRangeException();
					}

					this.previousTime = value;
					this.currentTime = value;
					this.OnCurrentTimeChanged(EventArgs.Empty);
				}
			}
		}

		/// <summary>
		/// Gets the associated <see cref="SkinningData"/> for the <see cref="AnimationPlayer"/>.
		/// </summary>
		protected SkinningData SkinningData
		{
			get { return this.skinningData; }
		}

		/// <summary>
		/// Gets the previous time of the <see cref="AnimationPlayer"/>.
		/// </summary>
		protected TimeSpan PreviousTime
		{
			get { return this.previousTime; }
		}

		/// <summary>
		/// Starts playing the specified clip, notifying subscribers if necessary.
		/// </summary>
		/// <param name="name">The name of the <see cref="AnimationClip"/> to start playing.</param>
		public void StartClip(string name)
		{
			// Call overloaded method.
			this.StartClip(this.skinningData.AnimationClips[name], true, null);
		}

		/// <summary>
		/// Starts playing the specified clip, notifying subscribers if necessary.
		/// </summary>
		/// <param name="name">The name of the <see cref="AnimationClip"/> to start playing.</param>
		/// <param name="loop">Indicates if the animation should be looped.</param>
		public void StartClip(string name, bool loop)
		{
			// Call overloaded method.
			this.StartClip(this.skinningData.AnimationClips[name], loop, null);
		}

		/// <summary>
		/// Starts playing the specified clip, notifying subscribers if necessary.
		/// </summary>
		/// <param name="name">The name of the <see cref="AnimationClip"/> to start playing.</param>
		/// <param name="callback">The <see cref="IAnimationPlayerCallback"/> to use for <see cref="AnimationEvent"/> notifcations.</param>
		public void StartClip(string name, IAnimationPlayerCallback callback)
		{
			// Call overloaded method.
			this.StartClip(this.skinningData.AnimationClips[name], true, callback);
		}

		/// <summary>
		/// Starts playing the specified clip, notifying subscribers if necessary.
		/// </summary>
		/// <param name="name">The name of the <see cref="AnimationClip"/> to start playing.</param>
		/// <param name="loop">Indicates if the animation should be looped.</param>
		/// <param name="callback">The <see cref="IAnimationPlayerCallback"/> to use for <see cref="AnimationEvent"/> notifcations.</param>
		public void StartClip(string name, bool loop, IAnimationPlayerCallback callback)
		{
			// Call overloaded method.
			this.StartClip(this.skinningData.AnimationClips[name], loop, callback);
		}

		/// <summary>
		/// Starts playing the specified clip, notifying subscribers if necessary.
		/// </summary>
		/// <param name="clip">The <see cref="AnimationClip"/> to start playing.</param>
		/// <param name="loop">Indicates if the animation should be looped.</param>
		/// <param name="callback">The <see cref="IAnimationPlayerCallback"/> to use for <see cref="AnimationEvent"/> notifcations.</param>
		protected virtual void StartClip(AnimationClip clip, bool loop, IAnimationPlayerCallback callback)
		{
			if (this.currentClip != clip)
			{
				// Hold onto the new playing clip and reset the animation times.
				this.currentClip = clip;
				this.previousTime = TimeSpan.Zero;
				this.currentTime = TimeSpan.Zero;

				// Notify subscibers of changes.
				this.OnCurrentClipChanged(EventArgs.Empty);
				this.OnCurrentTimeChanged(EventArgs.Empty);
			}

			// Always hold ontp new loop and callback values.
			this.loop = loop;
			this.callback = callback;
		}

		public virtual void Update(GameTime gameTime)
		{
			// Hold onto the previous time.
			this.previousTime = this.currentTime;

			// Add the game's elapsed time to the current time.
			TimeSpan time = gameTime.ElapsedGameTime + this.currentTime;

			// Check for looping.
			if (this.loop)
			{
				while (time < TimeSpan.Zero)
				{
					time += this.currentClip.Duration;
				}
				while (time >= this.currentClip.Duration)
				{
					time -= this.currentClip.Duration;
				}
			}
			else
			{
				time = TimeSpanExtension.Clamp(
					time, 
					TimeSpan.Zero, 
					this.currentClip.Duration);
			}

			// Set the current time.
			this.currentTime = time;
			this.OnCurrentTimeChanged(EventArgs.Empty);

			// Notify subscribers of animation events that occurred during
			// this frame.
			if (this.callback != null)
			{
				int index, count;
				if (this.currentClip.Events.FindSegment(
					this.previousTime, 
					this.currentTime, 
					out index, 
					out count))
				{
					for (int i = index; i < count; i++)
					{
						this.callback.OnEvent(gameTime, this.currentClip.Events[i]);
					}
				}
			}
		}

		/// <summary>
		/// Adjusts the specified <see cref="Effect"/> by assigning the animation information to
		/// shader constants.
		/// </summary>
		/// <param name="effect">The source <see cref="Effect"/>.</param>
		public virtual void AdjustEffect(Effect effect) { }

		/// <summary>
		/// Occurs when the <see cref="AnimationPlayer.CurrentClip"/> changed.
		/// </summary>
		/// <param name="e"></param>
		protected virtual void OnCurrentClipChanged(EventArgs e) { }

		/// <summary>
		/// Occurs when the <see cref="AnimationPlayer.CurrentTime"/> changed.
		/// </summary>
		/// <param name="e"></param>
		protected virtual void OnCurrentTimeChanged(EventArgs e) { }
	}
}
